<?php

Raise::load('core.RaiseDataType');
Raise::load('core.IRaiseComparable');
Raise::load('core.RaiseTimeSpan');

/**
 * RaiseDateTime class
 * representing date and time
 *
 * @author Sam-Mauris Yong <hellclanner at live dot com>
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package Raise.Core.DateTime
 * @since 1.2
 */
class RaiseDateTime extends RaiseDataType implements IRaiseComparable {

    /**
     * Day of the month (1 to 31)
     * @var integer
     */
    private $day = 0;

    /**
     * Month of the year (1 to 12)
     * @var integer
     */
    private $month = 0;

    /**
     * The year
     * @var integer
     */
    private $year = 0;

    /**
     * Hour of the day (0 to 23), 24-hour format
     * @var integer
     */
    private $hour = 0;

    /**
     * Minutes of the hour (0 to 59)
     * @var integer
     */
    private $minute = 0;

    /**
     * Seconds of the minute (0 to 59)
     * @var integer
     */
    private $second = 0;

    /**
     * milliseconds
     * @var integer
     */
    private $millisecond = 0;

    /**
     * Timezone that the RaiseDateTime is in
     * @var double Timezone in hours
     */
    private $timezone = 0;

    /**
     * The timezone component of the RaiseDateTime
     * @param double $a An integer that represents number of seconds in terms of time zone offset (1.5 for 1 hour 30 minutes)
     * @return double
     */
    public function timezone($a = null){
        if(func_num_args() == 1){
            self::convertTimezone($this, $a);
        }
        return $this->timezone;
    }

    /**
     * The millisecond component of the RaiseDateTime
     * @param integer $a An integer that represents millisecond (1000th of a second)
     * @return integer
     */
    public function millisecond($a = null){
        if(func_num_args() == 1){
            $this->millisecond = $a;
        }
        return $this->millisecond;
    }

    /**
     * The seconds component of the RaiseDateTime
     * @param integer $a An integer from 0 to 59 that represents the 60 seconds in 1 minute
     * @return integer
     */
    public function second($a = null){
        if(func_num_args() == 1){
            $this->second = $a;
        }
        return $this->second;
    }

    /**
     * The minute component of the RaiseDateTime
     * @param integer $a An integer from 0 to 59 that represents the 60 minutes in 1 hour
     * @return integer
     */
    public function minute($a = null){
        if(func_num_args() == 1){
            $this->minute = $a;
        }
        return $this->minute;
    }

    /**
     * The hour component of the RaiseDateTime
     * @param integer $a An integer from 0 to 23 that represents 24 hours
     * @return integer
     */
    public function hour($a = null){
        if(func_num_args() == 1){
            $this->hour = $a;
        }
        return $this->hour;
    }
    
    /**
     * The year component of the RaiseDateTime
     * @param integer $a An integer that represents the year
     * @return integer
     */
    public function year($a = null){
        if(func_num_args() == 1){
            $this->year = $a;
        }
        return $this->year;
    }

    /**
     * The month component of the RaiseDateTime
     * @param integer $a An integer from 1 to 12 representing the various months.
     * @return integer
     */
    public function month($a = null){
        if(func_num_args() == 1){
            $this->month = $a;
        }
        return $this->month;
    }

    /**
     * The day component of the RaiseDateTime
     * @param integer $a An integer from 1 to 31 representing the days of a month
     * @return integer
     */
    public function day($a = null){
        if(func_num_args() == 1){
            $this->day = $a;
        }
        return $this->day;
    }

    /**
     * Get the RaiseDateTime of this exact instant
     * @return RaiseDateTime
     */
    public static function now() {
        static $now = null;
        if(!$now){
            $now = self::fromTimestamp(false);
        }
        return $now;
    }

    /**
     * Convert a UNIX Epoch Timestamp to RaiseDateTime
     * Note that when converting from a seconds-based timestamp to RaiseDateTime, timezone and millisecond information cannot be captured and will be both set to 0.
     * @param integer $ts UNIX Epoch Timestamp. If not set, the conversion will be done from the current time.
     * @return RaiseDateTime
     */
    public static function fromTimestamp($ts) {
        if (!$ts) {
            $ts = time();
        }
        $dt = new RaiseDateTime();
        $dt->day((int) gmdate('j', $ts));
        $dt->month((int) gmdate('n', $ts));
        $dt->year((int) gmdate('o', $ts));

        $dt->hour((int) gmdate('G', $ts));
        $dt->minute((int) gmdate('i', $ts));
        $dt->second((int) gmdate('s', $ts));

        $dt->millisecond(0);
        $dt->timezone(0);
        
        return $dt;
    }

    /**
     * Change the timezone of a RaiseDateTime
     * @param RaiseDateTime $dateTime The date time object
     * @param double $target The destination timezone to set the date time to. Can in the form of hours
     * return RaiseDateTime
     */
    public static function convertTimezone($dateTime, $target){
        if($dateTime instanceof RaiseDateTime){
            if($target < -12 || $target > 12){
                throw new RaiseInvalidArgumentException('$target Timezone must be between -12 and 12 inclusive when converting between timezones');
            }else{
                if($dateTime->timezone == $target){
                    return $dateTime;
                }
                $diff = $dateTime->timeZone + $target;
                $ts = new RaiseTimeSpan(abs($diff) * 3600);
                if($diff > 0){
                    $dt = $dateTime->subtract($ts);
                }else{
                    $dt = $dateTime->add($ts);
                }
                $dt->timezone = $target;
                return $dt;
            }
        }else{
            throw new RaiseInvalidArgumentException('$dateTime must be a RaiseDateTime instance when converting between timezones.');
        }
    }

    /**
     * Converts a string into RaiseDateTime
     * @param string $s The string to convert
     * @return RaiseDateTime
     */
    public static function fromString($s) {
        return self::fromTimestamp(strtotime($s));
    }

    /**
     * Converts this RaiseDateTime object to UNIX Epoch Timestamp
     * @return integer
     */
    public function toTimestamp() {
        $hour = $this->hour() - $this->timezone;
        if($hour < 0){
            $hour -= 24;
        }
        $ts = gmmktime($hour, $this->minute(), $this->second(), $this->month(), $this->day(), $this->year());
        return $ts;
    }

    /**
     * Creates an ISO8601 RaiseDateTime string
     * @link http://www.iso.org/iso/date_and_time_format
     * @return string
     */
    public function toISO8601() {
        return gmdate('Y-m-d\TH:i:s', $this->toTimestamp()) .
                ($this->timezone > 0 ?
                '+' . sprintf('%02d', floor(abs($this->timezone))) . ':' . sprintf('%02d', ceil(abs($this->timezone)-floor(abs($this->timezone))) * 60) :
                '-' . sprintf('%02d', floor(abs($this->timezone))) . ':' . sprintf('%02d', ceil(abs($this->timezone)-floor(abs($this->timezone))) * 60)  );
    }

    /**
     * Creates an RFC822 (updated by RFC 1123) RaiseDateTime string
     * @link http://www.freesoft.org/CIE/RFC/1945/14.htm
     * @return string
     */
    public function toRFC822(){
       return gmdate(DATE_RFC822, $this->toTimestamp());
    }

    /**
     * Check if a year is leap year
     * @param integer|RaiseDateTime $year The year or RaiseDateTime to check
     * @return boolean TRUE if the year is a leap year, FALSE otherwise
     */
    public static function isLeapYear($year){
        if($year instanceof RaiseDateTime){
            $year = $year->year();
        }
        return (($year % 4 == 0) && ($year % 100 != 0) || ($year % 400 == 0));
    }

    /**
     * Get the number of days in a month, considering whether the year is a leap year or not
     * @param integer|RaiseDateTime $month The month or the RaiseDateTime to check
     * @param integer $year (optional) The year. Ignored when first parameter is RaiseDateTime
     * @return integer The number of days in that month of that year
     */
    public static function daysInMonth($month, $year = null){
        if($month instanceof RaiseDateTime){
            $year = $month->year();
            $month = $month->month();
        }
        $mapping = array(
            31, (self::isLeapYear($year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
        );
        return $mapping[$month];
    }

    /**
     * Add a period of time RaiseTimeSpan to the current date time
     * @param RaiseTimeSpan $dt The RaiseTimeSpan to add
     * @return RaiseDateTime The result RaiseDateTime that resulted from the add operation.
     */
    public function add($dt){
        $new = $this->raiseClone();
        if($dt instanceof RaiseTimeSpan){
            $new->millisecond($new->millisecond() + $dt->millisecond());
            if($new->millisecond > 999){
                $new->millisecond -= 1000;
                $new->second++;
            }

            $new->second($new->second() + $dt->second());
            if($new->second > 59){
                $new->second -= 60;
                $new->minute++;
            }

            $new->minute($new->minute() + $dt->minute());
            if($new->minute > 59){
                $new->minute -= 60;
                $new->hour++;
            }

            $new->hour($new->hour() + $dt->hour());
            if($new->hour > 23){
                $new->hour -= 24;
                $new->day++;
            }

            $new->day($new->day() + $dt->day());
            $md = self::daysInMonth($new->month, $new->year);
            if($new->day > $md){
                $new->day -= $md;
                $new->month++;
            }

            if($new->month > 12){
                $new->month -= 12;
                $new->year++;
            }
        }else{
            throw new RaiseInvalidArgumentException('RaiseDateTime::add() expecting 1st parameter to be RaiseTimeSpan.');
        }
        
        return $new;
    }

    /**
     * Subtract another RaiseDateTime or RaiseTimeSpan from the current one
     * @param RaiseDateTime|RaiseTimeSpan $dt The RasieDateTime or RaiseTimeSpan to subtract
     * @return RaiseTimeSpan|RaiseDateTime The result of the subtract operation. If $dt is a RaiseDateTime, RaiseTimeSpan will be returned. If $dt is a RaiseTimeSpan, RaiseDateTime will be returned instead.
     */
    public function subtract($dt){
        $new = $this->raiseClone();
        if($dt instanceof RaiseDateTime){
            if($this->compareTo($dt) == 1){
                throw new RaiseInvalidArgumentException('Cannot subtract a date from a date earlier.');
            }
            $new->millisecond($new->millisecond() - $dt->millisecond());
            if($new->millisecond < 0){
                $new->millisecond = 1000 + $new->millisecond;
                $new->second--;
            }

            $new->second($new->second() - $dt->second());
            if($new->second < 0){
                $new->second = 60 + $new->second;
                $new->minute--;
            }
            
            $new->minute($new->minute() - $dt->minute());
            if($new->minute < 0){
                $new->minute = 60 + $new->minute;
                $new->hour--;
            }

            $new->hour($new->hour() - $dt->hour());
            if($new->hour < 0){
                $new->hour = 24 + $new->hour;
                $new->day--;
            }

            $new->day($new->day() - $dt->day());
            if($new->day < 0){
                $new->month--;
                $new->day = self::daysInMonth(($new->month < 1 ? 12 : $new->month), ($new->month < 1 ? $new->year - 1 : $new->year)) + $new->day;
            }

            $new->month($new->month() - $dt->month());
            if($new->month < 0){
                $new->month = 12 + $new->month;
                $new->year--;
            }

            $new->year($new->year() - $dt->year());

            $ts = new RaiseTimeSpan(0);
            $ts->day($new->year() * 365 + $new->month() * 30 + $new->day());
            $ts->hour($new->hour());
            $ts->minute($new->minute());
            $ts->second($new->second());
            $ts->millisecond($new->millisecond());

            return $ts;
        }elseif($dt instanceof RaiseTimeSpan){
            
            $new->millisecond($new->millisecond() - $dt->millisecond());
            if($new->millisecond < 0){
                $new->millisecond = 1000 + $new->millisecond;
                $new->second--;
            }

            $new->second($new->second() - $dt->second());
            if($new->second < 0){
                $new->second = 60 + $new->second;
                $new->minute--;
            }

            $new->minute($new->minute() - $dt->minute());
            if($new->minute < 0){
                $new->minute = 60 + $new->minute;
                $new->hour--;
            }

            $new->hour($new->hour() - $dt->hour());
            if($new->hour < 0){
                $new->hour = 24 + $new->hour;
                $new->day--;
            }

            $new->day($new->day() - $dt->day());
            if($new->day < 0){
                $new->month--;
                $new->day = self::daysInMonth(($new->month < 1 ? 12 : $new->month), ($new->month < 1 ? $new->year - 1 : $new->year)) + $new->day;
            }
            
            if($new->month < 1){
                $new->month = 12 + $new->month;
                $new->year--;
            }
            
            return $new;
        }
    }

    /**
     * Formats the RaiseDateTime object and return as string
     * @param string $format
     * @return string
     */
    public function toFormat($format) {
        $ts = $this->toTimestamp();
        return gmdate($format, $ts);
    }

    /**
     * Return RaiseDateTime as a ISO 8601 string
     */
    public function __toString(){
        return $this->toISO8601();
    }

    /**
     * Returns a UNIX Timestamp with microseconds
     * @return double
     * @static
     */
    public static function getMicrotime(){
        $usec = 0;
        $sec = 0;
        list($usec, $sec) = explode(' ', microtime());
        return ((double)$usec + (double)$sec);
    }

    /**
     * Compare to another RaiseDateTime
     * @param RaiseDateTime $date
     * @return integer The comparison result: 1 if $date is more than $this, 0 if equal, and -1 if $this is more than $date.
     */
    public function compareTo($date) {
        if($date->year > $this->year){
            return 1;
        }
        if($date->year < $this->year){
            return -1;
        }
        if($date->month > $this->month){
            return 1;
        }
        if($date->month < $this->month){
            return -1;
        }
        if($date->day > $this->day){
            return 1;
        }
        if($date->day < $this->day){
            return -1;
        }
        if($date->hour > $this->hour){
            return 1;
        }
        if($date->hour < $this->hour){
            return -1;
        }
        if($date->minute > $this->minute){
            return 1;
        }
        if($date->minute < $this->minute){
            return -1;
        }
        if($date->second > $this->second){
            return 1;
        }
        if($date->second < $this->second){
            return -1;
        }
        if($date->millisecond > $this->millisecond){
            return 1;
        }
        if($date->millisecond < $this->millisecond){
            return -1;
        }
        return 0;
    }


}